package com.bluexmicro.ota.dfu

import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.content.Context
import android.util.Log
import com.bluexmicro.bluetooth.BluetoothCentral
import com.bluexmicro.bluetooth.model.ConnectionPriority
import com.bluexmicro.bluetooth.model.PhyType
import com.bluexmicro.bluetooth.model.ScanMode
import com.bluexmicro.bluetooth.model.WriteType
import com.bluexmicro.bluetooth.parZip
import com.bluexmicro.bluetooth.peripheral.ConnectionState
import com.bluexmicro.bluetooth.peripheral.PeripheralManager
import com.bluexmicro.bluetooth.toUInt8
import com.bluexmicro.ota.FirmwareHelper
import com.bluexmicro.ota.dfu.ExitAction.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withTimeout
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.UUID
import kotlin.Exception
import kotlin.math.max

class DfuProcess {

    private var chunkedSize = 20

    companion object {
        private val UUID_BX_OTA_SERVICE = UUID.fromString("0000ee00-d102-e111-9b23-00025b00a5c7")
        private val UUID_BX_COMMAND = UUID.fromString("0000ee01-d102-e111-9b23-00025b00a5c7")
        private val UUID_BX_DATA = UUID.fromString("0000ee02-d102-e111-9b23-00025b00a5c7")
        private const val CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"
        private val UUID_CLIENT_CHARACTERISTIC_CONFIG =
            UUID.fromString(CLIENT_CHARACTERISTIC_CONFIG)
        private const val COMMAND_PROPERTIES =
            BluetoothGattCharacteristic.PROPERTY_NOTIFY or BluetoothGattCharacteristic.PROPERTY_WRITE
        private const val DATA_PROPERTIES =
            BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE
        const val CMD_START = 0
        const val CMD_END = 1
        private const val CMD_CHECK_RECOVERY = 2
        private const val CMD_LAUNCH_RECOVERY = 3
        private const val CMD_EXIT = 4
    }

    private fun matchByAdvertise(macAddress: String, advertisingData: ByteArray?): Boolean {
        advertisingData ?: return false
        val macBytes = try {
            macAddress.split(":").map {
                it.toInt(16).toByte()
            }.toByteArray()
        } catch (e: Exception) {
            return false
        }

        if (macBytes.size != 6 || advertisingData.size < macBytes.size) return false

        for (i in 0..advertisingData.size - macBytes.size) {
            if (advertisingData.sliceArray(i until i + macBytes.size).contentEquals(macBytes)) {
                return true
            }
        }
        return false
    }


    /**
     * @param device 请填写当前App已连接的目标设备
     */
    suspend fun initialize(
        context: Context,
        device: BluetoothDevice
    ): Process {

        val triple =
            setupPeripheral(context, device.address)
        //可能要更换对象
        var peripheral = triple.first
        var commandHandler = triple.second
        var dataHandler = triple.third

        var recoveryMode = fetchRecoveryMode(peripheral, commandHandler)

        if (recoveryMode.not()) {

            // 请求进入 Recovery 模式
            recoveryMode = launchRecoveryMode(peripheral, commandHandler)
            if (recoveryMode.not()) throw IllegalStateException("Cannot enter recovery mode")

            //此时设备会重启，这里手动断开一次
            BluetoothCentral.releasePeripheral(peripheral)

            Log.d("initialize: ", "断开连接进入Recovery")

            delay(3000)
            BluetoothCentral.clearCache()
            BluetoothCentral.scanForPeripherals(ScanMode.LOW_LATENCY)
            var recoveryMacAddress: String? = null
            for (i in 0..4) {
                delay(1000L)
                val results = BluetoothCentral.fetchScanResults()
                val target = results.find {
                    matchByAdvertise(device.address, it.scanRecord?.bytes)
                }
                if (target != null) {
                    recoveryMacAddress = target.device.address
                    break
                }
            }
            BluetoothCentral.stopScan()

            if (recoveryMacAddress != null) {
                Log.d("initialize: ", "找到了目标设备：$recoveryMacAddress")
                setupPeripheral(context, recoveryMacAddress).apply {
                    peripheral = first
                    commandHandler = second
                    dataHandler = third
                }
            } else {
                Log.d("initialize: ", "未找到了目标设备")
            }


            //二次确认是否进入了 Recovery 模式
            recoveryMode = fetchRecoveryMode(peripheral, commandHandler)
            if (recoveryMode.not()) throw IllegalStateException("Cannot enter recovery mode")
        }

        return Process(context, peripheral, commandHandler, dataHandler)
    }


    inner class Process constructor(
        private val context: Context,
        private val peripheral: PeripheralManager.BluetoothPeripheral,
        private val commandHandler: BluetoothGattCharacteristic,
        private val dataHandler: BluetoothGattCharacteristic
    ) {

        fun update(zip: File, action: ExitAction = RebootEnterApp) = flow {
            val tasks = FirmwareHelper.unzipToFirmwares(context, zip)

            val sum = tasks.sumOf { it.rawData.size }
            var sentSize = 0
            for (task in tasks) {

                if (requestStart(peripheral, commandHandler, task).not()) {
                    throw IllegalStateException("Remote Device reject start")
                }

                val checkout = task.rawData.size.run {
                    val tmp = (this.toFloat() / 1024 / 1024 * 500).toLong()
                    max(5000L, tmp)
                }

                delay(50)
                val pending = task.rawData.toList().chunked(chunkedSize)
                var progress = 0
                for (i in pending.indices) {
                    val byteArray = pending[i].toByteArray()
                    peripheral.write(dataHandler, WriteType.WithoutResponse, byteArray)

                    //todo 可以减少emit的次数，来提高速度（如果刷新UI）
//                    sentSize += byteArray.size
//                    emit(sentSize.toFloat() / sum)
                    sentSize += byteArray.size
                    val tmp = sentSize.toFloat() / sum
                    val tmp2 = (tmp * 100).toInt()
                    if (tmp2 != progress) {
                        progress = tmp2
                        emit(tmp)
                    }
                }
                delay(50)

                if (requestEnd(peripheral, commandHandler, checkout).not()) {
                    throw IllegalStateException("Remote Device reject end")
                }
            }
            delay(50)
            try {
                val cmd = byteArrayOf(CMD_EXIT.toByte(), 0x00.toByte(), action.value)
                peripheral.write(commandHandler, WriteType.WithResponse, cmd)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            emit(1f)

            BluetoothCentral.releasePeripheral(peripheral)
        }
    }

    private suspend fun setupPeripheral(
        context: Context,
        macAddress: String
    ): Triple<PeripheralManager.BluetoothPeripheral, BluetoothGattCharacteristic, BluetoothGattCharacteristic> {
        BluetoothCentral.setup(context)
        return withTimeout(12000L) {
            //初始化设备
            val peripheral = BluetoothCentral.retrievePeripherals(context, macAddress)
                ?: throw IllegalArgumentException("mac address invalid or please scan device first")

            //连接设备
            var connected = peripheral.state.value == ConnectionState.CONNECTED
            if (connected.not()) {
                for (i in 0..1) {
                    //todo timeout
                    BluetoothCentral.connect(context, peripheral)
                    if (peripheral.state.value == ConnectionState.CONNECTED) {
                        connected = true
                        break
                    }
                }
            }
            if (!connected) throw IllegalStateException("Cannot connect device with $macAddress")
            delay(50)

            //同上
            try {
                val size = peripheral.requestMTU(251).getOrNull()?:23
                //在Android S以上版本，越是接近最高速度失败率越高
                chunkedSize = max(20, size - 23)
                delay(50)
                //设置连接参数，不成功也是可以的，但是会影响速度
                peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
                delay(50)
                peripheral.requestPhy(PhyType.LE_2M.mask, PhyType.LE_2M.mask)
            } catch (e: Exception) {
                e.printStackTrace()
            }

            delay(50)

            //初始化服务特征
            val services = peripheral.discoverServices().getOrNull()
            val service = services?.find { it.uuid.equals(UUID_BX_OTA_SERVICE) }
            val command = service?.getCharacteristic(UUID_BX_COMMAND)
            val data = service?.getCharacteristic(UUID_BX_DATA)
            if (command == null || data == null) throw IllegalStateException("Invalid Device")
            val flag1 = command.properties and COMMAND_PROPERTIES > 0
            val flag2 = data.properties and DATA_PROPERTIES > 0
            if (flag1.not() || flag2.not()) throw IllegalStateException("Invalid Device")
            delay(50)
            //开启Notification
            peripheral.setNotification(command, true)
            delay(50)
            val descriptor = command.getDescriptor(UUID_CLIENT_CHARACTERISTIC_CONFIG)
                ?: throw IllegalStateException("Invalid Device")
            peripheral.writeDescriptor(
                descriptor,
                BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            )
            Triple(peripheral, command, data)
        }
    }


    private suspend fun fetchRecoveryMode(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic
    ): Boolean {
        val cmd = byteArrayOf(CMD_CHECK_RECOVERY.toByte(), 0x00.toByte())
        val rsp = request(peripheral, commandHandler, cmd)
        return rsp[2].toUInt8() == 0
    }

    private suspend fun launchRecoveryMode(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic
    ): Boolean {
        val cmd = byteArrayOf(CMD_LAUNCH_RECOVERY.toByte(), 0x00.toByte())
        val rsp = request(peripheral, commandHandler, cmd)
        return rsp[2].toUInt8() == 0
    }


    private suspend fun requestStart(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic,
        bean: FirmwareBean
    ): Boolean {
        val bb = ByteBuffer.allocate(11).order(ByteOrder.LITTLE_ENDIAN)
        bb.put(CMD_START.toByte())
            .put(0)
            .put(bean.idx)
            .putInt(bean.rawData.size)
            .putInt(bean.crc32)
        val rsp = request(peripheral, commandHandler, bb.array())
        return rsp[2].toUInt8() == 0
    }

    private suspend fun requestEnd(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic,
        timeout: Long = 5000L
    ): Boolean {
        val cmd = byteArrayOf(CMD_END.toByte(), 0x00.toByte())
        val rsp = try {
            request(peripheral, commandHandler, cmd, timeout)
        } catch (e: Exception) {
            byteArrayOf(CMD_END.toByte(), 0x80.toByte(), 1)
        }
//        val rsp = request(peripheral, commandHandler, cmd)
        return rsp[2].toUInt8() == 0
    }


    private suspend fun request(
        peripheral: PeripheralManager.BluetoothPeripheral,
        commandHandler: BluetoothGattCharacteristic,
        cmd: ByteArray,
        timeout: Long = 5000L
    ): ByteArray {
        return withTimeout(timeout) {
            parZip(
                { peripheral.write(commandHandler, WriteType.WithResponse, cmd) },
                {
                    BluetoothCentral.observeCharacteristicValue(
                        peripheral.device.address,
                        commandHandler.service.uuid.toString(),
                        commandHandler.uuid.toString()
                    ).first {
                        it.value.size >= 3 && it.value[1].toUInt8() == 0x80 && it.value[0] == cmd[0]
                    }
                }
            ) { _, bluetoothValue ->
                bluetoothValue.value
            }
        }
    }
}